﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using elmah2.core.data;
using System.IO;
using System.Xml;
using System.Collections.Specialized;

namespace elmah2.core.encoders
{
  public static class XmlEncoder
  {
    /// <summary>
    /// Decodes an <see cref="Error"/> object from its default XML 
    /// representation.
    /// </summary>
    public static Error DecodeString(string xml)
    {
      using (StringReader sr = new StringReader(xml))
      {
        XmlTextReader reader = new XmlTextReader(sr);

        if (!reader.IsStartElement("error"))
          throw new ApplicationException("The error XML is not in the expected format.");

        return Decode(reader);
      }
    }

    /// <summary>
    /// Decodes an <see cref="Error"/> object from its XML representation.
    /// </summary>
    public static Error Decode(XmlReader reader)
    {
      if (reader == null)
        throw new ArgumentNullException("reader");

      if (!reader.IsStartElement())
        throw new ArgumentException("Reader is not positioned at the start of an element.", "reader");

      Error error = new Error();
      ReadXmlAttributes(reader, error);

      bool isEmpty = reader.IsEmptyElement;
      reader.Read();

      if (!isEmpty)
      {
        ReadInnerXml(reader, error);
        reader.ReadEndElement();
      }

      return error;
    }

    /// <summary>
    /// Reads the error data in XML attributes.
    /// </summary>
    private static void ReadXmlAttributes(XmlReader reader, Error error)
    {
      if (reader == null)
        throw new ArgumentNullException("reader");

      if (!reader.IsStartElement())
        throw new ArgumentException("Reader is not positioned at the start of an element.", "reader");

      error.ApplicationName = reader.GetAttribute("application");
      error.HostName = reader.GetAttribute("host");
      error.Type = reader.GetAttribute("type");
      error.Message = reader.GetAttribute("message");
      error.Source = reader.GetAttribute("source");
      error.Detail = reader.GetAttribute("detail");
      error.User = reader.GetAttribute("user");
      string timeString = reader.GetAttribute("time");
      error.Time = timeString.Length == 0 ? new DateTime() : XmlConvert.ToDateTime(timeString, XmlDateTimeSerializationMode.Local);
      string statusCodeString = reader.GetAttribute("statusCode");
      error.StatusCode = string.IsNullOrEmpty(statusCodeString) ? 0 : XmlConvert.ToInt32(statusCodeString);
      error.WebHostHtmlMessage = reader.GetAttribute("webHostHtmlMessage");
    }

    /// <summary>
    /// Reads the error data in child nodes.
    /// </summary>
    private static void ReadInnerXml(XmlReader reader, Error error)
    {
      if (reader == null)
        throw new ArgumentNullException("reader");

      while (reader.IsStartElement())
      {
        NameValueCollection collection;

        switch (reader.LocalName)
        {
          case "serverVariables": collection = error.ServerVariables; break;
          case "queryString": collection = error.QueryString; break;
          case "form": collection = error.Form; break;
          case "cookies": collection = error.Cookies; break;
          default: return;
        }

        if (reader.IsEmptyElement)
          reader.Read();
        else
          UpcodeTo(reader, collection);
      }
    }

    /// <summary>
    /// Encodes the default XML representation of an <see cref="Error"/> 
    /// object to a string.
    /// </summary>
    public static string EncodeString(Error error)
    {
      StringWriter sw = new StringWriter();

      XmlWriterSettings settings = new XmlWriterSettings();
      settings.Indent = true;
      settings.NewLineOnAttributes = true;
      settings.CheckCharacters = false;
      settings.OmitXmlDeclaration = true;
      XmlWriter writer = XmlWriter.Create(sw, settings);

      try
      {
        writer.WriteStartElement("error");
        Encode(error, writer);
        writer.WriteEndElement();
        writer.Flush();
      }
      finally
      {
        writer.Close();
      }

      return sw.ToString();
    }

    /// <summary>
    /// Encodes the XML representation of an <see cref="Error"/> object.
    /// </summary>
    public static void Encode(Error error, XmlWriter writer)
    {
      if (writer == null)
        throw new ArgumentNullException("writer");

      if (writer.WriteState != WriteState.Element)
        throw new ArgumentException("Writer is not in the expected Element state.", "writer");

      WriteXmlAttributes(error, writer);
      WriteInnerXml(error, writer);
    }

    /// <summary>
    /// Writes the error data that belongs in XML attributes.
    /// </summary>
    private static void WriteXmlAttributes(Error error, XmlWriter writer)
    {
      if (writer == null)
        throw new ArgumentNullException("writer");

      WriteXmlAttribute(writer, "application", error.ApplicationName);
      WriteXmlAttribute(writer, "host", error.HostName);
      WriteXmlAttribute(writer, "type", error.Type);
      WriteXmlAttribute(writer, "message", error.Message);
      WriteXmlAttribute(writer, "source", error.Source);
      WriteXmlAttribute(writer, "detail", error.Detail);
      WriteXmlAttribute(writer, "user", error.User);
      if (error.Time != DateTime.MinValue)
        WriteXmlAttribute(writer, "time", XmlConvert.ToString(error.Time.ToUniversalTime(), @"yyyy-MM-dd\THH:mm:ss.fffffff\Z"));
      if (error.StatusCode != 0)
        WriteXmlAttribute(writer, "statusCode", XmlConvert.ToString(error.StatusCode));
      WriteXmlAttribute(writer, "webHostHtmlMessage", error.WebHostHtmlMessage);
    }

    /// <summary>
    /// Writes the error data that belongs in child nodes.
    /// </summary>
    private static void WriteInnerXml(Error error, XmlWriter writer)
    {
      if (writer == null)
        throw new ArgumentNullException("writer");

      WriteCollection(writer, "serverVariables", error.ServerVariables);
      WriteCollection(writer, "queryString", error.QueryString);
      WriteCollection(writer, "form", error.Form);
      WriteCollection(writer, "cookies", error.Cookies);
    }

    private static void WriteCollection(XmlWriter writer, string name, NameValueCollection collection)
    {
      if (collection != null && collection.Count != 0)
      {
        writer.WriteStartElement(name);
        Encode(collection, writer);
        writer.WriteEndElement();
      }
    }

    private static void WriteXmlAttribute(XmlWriter writer, string name, string value)
    {
      if (value != null && value.Length != 0)
        writer.WriteAttributeString(name, value);
    }

    /// <summary>
    /// Encodes an XML representation for a 
    /// <see cref="NameValueCollection" /> object.
    /// </summary>
    private static void Encode(NameValueCollection collection, XmlWriter writer)
    {
      if (collection == null)
        throw new ArgumentNullException("collection");

      if (writer == null)
        throw new ArgumentNullException("writer");

      if (collection.Count == 0)
      {
        return;
      }

      foreach (string key in collection.Keys)
      {
        writer.WriteStartElement("item");
        writer.WriteAttributeString("name", key);

        string[] values = collection.GetValues(key);

        if (values != null)
        {
          foreach (string value in values)
          {
            writer.WriteStartElement("value");
            writer.WriteAttributeString("string", value);
            writer.WriteEndElement();
          }
        }

        writer.WriteEndElement();
      }
    }

    /// <summary>
    /// Updates an existing <see cref="NameValueCollection" /> object from
    /// its XML representation.
    /// </summary>
    private static void UpcodeTo(XmlReader reader, NameValueCollection collection)
    {
      if (collection == null)
        throw new ArgumentNullException("collection");

      if (reader == null)
        throw new ArgumentNullException("reader");

      reader.Read();

      while (reader.IsStartElement("item"))
      {
        string name = reader.GetAttribute("name");
        bool isNull = reader.IsEmptyElement;

        reader.Read(); // <item>

        if (!isNull)
        {

          while (reader.IsStartElement("value")) // <value ...>
          {
            string value = reader.GetAttribute("string");
            collection.Add(name, value);
            reader.Read();
          }

          reader.ReadEndElement(); // </item>
        }
        else
        {
          collection.Add(name, null);
        }
      }

      reader.ReadEndElement();
    }
  }
}
